package org.bennedum.SimpleMCClient.protocol;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;
import java.util.Map;
import org.bennedum.SimpleMCClient.Main;

/**
 *
 * @author frdfsnlght <frdfsnlght@gmail.com>
 */
public class Packet {

    private static final Map<Integer,Integer> FIXED = new HashMap<Integer,Integer>();
    private static final Map<Integer,int[]> STRING = new HashMap<Integer,int[]>();

    // Packets taken from http://mc.kev009.com/Protocol

    static {
        FIXED.put(0x000, 1);                     // Keepalive
        FIXED.put(0x005, 11);                    // Entity equipment
        FIXED.put(0x006, 13);                    // Spawn position
        FIXED.put(0x007, 10);                    // Use entity?
        FIXED.put(0x008, 3);                     // Update health
        FIXED.put(0x009, 2);                     // Respawn
        FIXED.put(0x00A, 2);                     // Player
        FIXED.put(0x00B, 34);                    // Player position
        FIXED.put(0x00C, 10);                    // Player look
        FIXED.put(0x00E, 12);                    // Player digging
        FIXED.put(0x010, 3);                     // Holding change
        FIXED.put(0x011, 15);                    // Use bed
        FIXED.put(0x012, 6);                     // Animation
        FIXED.put(0x013, 6);                     // Entity action
        STRING.put(0x014, new int[] {23, 5});    // Named entity spawn
        FIXED.put(0x015, 25);                    // Pickup spawn
        FIXED.put(0x016, 9);                     // Collect item
        STRING.put(0x019, new int[] {23, 5});    // Painting
        FIXED.put(0x01B, 19);                    // Stance update?
        FIXED.put(0x01C, 11);                    // Entity velocity?
        FIXED.put(0x01D, 5);                     // Destroy entity
        FIXED.put(0x01E, 5);                     // Entity
        FIXED.put(0x01F, 8);                     // Entity relative move
        FIXED.put(0x020, 7);                     // Entity look
        FIXED.put(0x021, 10);                    // Entity look and relative move
        FIXED.put(0x022, 19);                    // Entity teleport
        FIXED.put(0x026, 6);                     // Entity status?
        FIXED.put(0x027, 9);                     // Attach entity?

        FIXED.put(0x032, 10);                    // Pre-chunk
        FIXED.put(0x035, 12);                    // Block change
        FIXED.put(0x036, 13);                    // Block action

        FIXED.put(0x03D, 18);                    // Sound effect

        FIXED.put(0x046, 2);                     // New/invalid state
        FIXED.put(0x047, 18);                    // Thunderbolt

        STRING.put(0x064, new int[] {6, 2});     // Open window
        FIXED.put(0x065, 2);                     // Close window
        FIXED.put(0x069, 6);                     // Update progress
        FIXED.put(0x06A, 5);                     // Transaction

        FIXED.put(0x0C8, 6);                     // Increment statistic
    }

    public static Packet decode(byte[] input) throws PacketException {
        if (input == null) return null;
        if (input.length == 0) return null;
        int type = getByteAsInt(input, 0);

        if (FIXED.containsKey(type)) {
            int len = FIXED.get(type);
            if (input.length < len) return null;
            return new Packet(input, 0, len);
        }
        if (STRING.containsKey(type)) {
            int[] params = STRING.get(type);
            int len = params[0];
            int pos = params[1];
            if (input.length < (pos + 1)) return null;
            len += (2 * getShort(input, pos));
            if (input.length < len) return null;
            return new Packet(input, 0, len);
        }
        String cName = Packet.class.getPackage().getName() + String.format(".Packet%02X", type);
        try {
            Class c = Class.forName(cName);
            try {
                Constructor cons = c.getConstructor(byte[].class);
                return (Packet)cons.newInstance(input);
            } catch (NoSuchMethodException nsme) {
                throw new IllegalArgumentException("invalid constructor for packet class " + c.getName());
            } catch (InstantiationException ie) {
                throw new IllegalArgumentException("unable to create packet class " + c.getName());
            } catch (IllegalAccessException iae) {
                throw new IllegalArgumentException("unable to create packet class " + c.getName());
            } catch (InvocationTargetException ite) {
                if (ite.getCause() instanceof IndexOutOfBoundsException)
                    // not enough data
                    return null;
                throw new PacketException(ite.getCause().getMessage());
            }
        } catch (ClassNotFoundException e) {
            throw new PacketException("unknown packet type: 0x%02X", type);
        }
    }

    protected static byte getByte(byte[] b, int p) {
        return b[p];
    }

    protected static void setByte(byte[] b, int p, byte v) {
        b[p] = v;
    }

    protected static int getByteAsInt(byte[] b, int p) {
        int i = (int)b[p];
        if (i < 0) i += 256;
        return i;
    }

    protected static void setIntAsByte(byte[] b, int p, int v) {
        if (v > 127)
            b[p] = (byte)(v - 256);
        else
            b[p] = (byte)v;
    }

    protected static short getShort(byte[] b, int p) {
        return (short)(
                    (((short)b[p++] & 0xFF) << 8) |
                    ((short)b[p++] & 0xFF)
                );
    }

    protected static void setShort(byte[] b, int p, short v) {
        b[p++] = (byte)(v >> 8);
        b[p++] = (byte)(v);
    }

    protected static int getInt(byte[] b, int p) {
        return (int)(
                    (((int)b[p++] & 0xFF) << 24) |
                    (((int)b[p++] & 0xFF) << 16) |
                    (((int)b[p++] & 0xFF) << 8) |
                    ((int)b[p++] & 0xFF)
                );
    }

    protected static void setInt(byte[] b, int p, int v) {
        b[p++] = (byte)(v >> 24);
        b[p++] = (byte)(v >> 16);
        b[p++] = (byte)(v >> 8);
        b[p++] = (byte)(v);
    }

    protected static long getLong(byte[] b, int p) {
        return (long)(
                    (((long)b[p++] & 0xFF) << 56) |
                    (((long)b[p++] & 0xFF) << 48) |
                    (((long)b[p++] & 0xFF) << 40) |
                    (((long)b[p++] & 0xFF) << 32) |
                    (((long)b[p++] & 0xFF) << 24) |
                    (((long)b[p++] & 0xFF) << 16) |
                    (((long)b[p++] & 0xFF) << 8) |
                    ((long)b[p++] & 0xFF)
                );
    }

    protected static void setLong(byte[] b, int p, long v) {
        b[p++] = (byte)(v >> 56);
        b[p++] = (byte)(v >> 48);
        b[p++] = (byte)(v >> 40);
        b[p++] = (byte)(v >> 32);
        b[p++] = (byte)(v >> 24);
        b[p++] = (byte)(v >> 16);
        b[p++] = (byte)(v >> 8);
        b[p++] = (byte)(v);
    }

    protected static float getFloat(byte[] b, int p) {
        DataInputStream is = new DataInputStream(new ByteArrayInputStream(b, p, 4));
        try {
            return is.readFloat();
        } catch (IOException e) {
            return 0;
        }
    }

    protected static void setFloat(byte[] b, int p, float v) {
        ByteArrayOutputStream s = new ByteArrayOutputStream(4);
        DataOutputStream os = new DataOutputStream(s);
        try {
            os.writeFloat(v);
        } catch (IOException e) {}
        System.arraycopy(s.toByteArray(), 0, b, p, 4);
    }

    protected static double getDouble(byte[] b, int p) {
        DataInputStream is = new DataInputStream(new ByteArrayInputStream(b, p, 8));
        try {
            return is.readDouble();
        } catch (IOException e) {
            return 0;
        }
    }

    protected static void setDouble(byte[] b, int p, double v) {
        ByteArrayOutputStream s = new ByteArrayOutputStream(8);
        DataOutputStream os = new DataOutputStream(s);
        try {
            os.writeDouble(v);
            os.close();
        } catch (IOException e) {}
        System.arraycopy(s.toByteArray(), 0, b, p, 8);
    }

    protected static boolean getBoolean(byte[] b, int p) {
        byte v = b[p];
        return ! (v == 0);
    }

    protected static void setBoolean(byte[] b, int p, boolean v) {
        b[p] = v ? (byte)1 : (byte)0;
    }


    /*
    protected static String getString8(byte[] b, int p) {
        short len = getShort(b, p);
        try {
            return new String(b, p + 2, len, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unable to decode UTF-8");
        }
    }

    protected static void setString8(byte[] b, int p, String s) {
        setShort(b, p, (short)s.length());
        try {
            byte[] src = s.getBytes("UTF-8");
            System.arraycopy(src, 0, b, p + 2, src.length);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unable to encode UTF-8");
        }
    }
    */

    protected static String getString16(byte[] b, int p) {
        short len = getShort(b, p);
        try {
            return new String(b, p + 2, len * 2, "UTF-16BE");
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unable to decode UTF-16BE");
        }
    }

    protected static void setString16(byte[] b, int p, String s) {
        setShort(b, p, (short)s.length());
        try {
            byte[] src = s.getBytes("UTF-16BE");
            System.arraycopy(src, 0, b, p + 2, src.length);
        } catch (UnsupportedEncodingException e) {
            throw new IllegalArgumentException("unable to encode UTF-16BE");
        }
    }

    protected static byte[] getMetadata(byte[] b, int p) {
        int start = p;
        for (byte x = b[p++]; x != 0x7f; x = b[p++]) {
            switch (x >> 5) {
                case 0: p++; break;       // byte
                case 1: p += 2; break;    // short
                case 2: p += 4; break;    // int
                case 3: p += 4; break;    // float
                case 4:                   // string16
                    short len = getShort(b, p);
                    p += 2 + (len * 2);
                    break;
                case 5: p += 2 + 1 + 2; break;    // short, byte, short
                case 6: p += 4 + 4 + 4; break;    // int, int, int
            }
        }
        byte[] meta = new byte[p - start];
        System.arraycopy(b, start, meta, 0, meta.length);
        return meta;
    }



    protected byte[] buffer = null;

    public Packet(int type, int length) {
        if (length < 1)
            throw new IllegalArgumentException("length must be greater than 1");
        buffer = new byte[length];
        setIntAsByte(buffer, 0, type);
    }

    protected Packet() {}

    protected Packet(byte[] in, int start, int length) {
        setBuffer(in, start, length);
    }

    protected final void setBuffer(byte[] in, int start, int length) {
        buffer = new byte[length];
        System.arraycopy(in, start, buffer, 0, length);
    }

    public byte[] getBuffer() {
        return buffer;
    }

    public int getType() {
        return getByteAsInt(buffer, 0);
    }

    public int getLength() {
        return buffer.length;
    }

    public byte getByte(int p) {
        return buffer[p];
    }

    public final void setByte(int p, byte b) {
        buffer[p] = b;
    }

    public short getShort(int p) {
        return getShort(buffer, p);
    }

    public int getInt(int p) {
        return getInt(buffer, p);
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder();
        sb.append(String.format("0x%02X (%s): ", getType(), getLength()));
        for (int i = 0; i < buffer.length; i++)
            sb.append(String.format("%02X ", buffer[i]));
        return sb.toString().trim();
    }

}
